package com.entityreborn.socpuppet.console;
import com.entityreborn.socbot.Colors;
import com.entityreborn.socpuppet.App;
import com.entityreborn.socpuppet.extensions.ConsoleCommand;
import com.entityreborn.socpuppet.extensions.ExtensionManager;
import com.entityreborn.socpuppet.extensions.ExtensionTracker;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.*;
import java.util.logging.Formatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.Completer;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiConsole;
/**
* A meta-class to handle all logging and input-related console improvements.
* Based off of Glowstone's implementation.
*/
public final class ConsoleManager {
private static ConsoleManager instance;
public synchronized static ConsoleManager getInstance() {
if (instance == null) {
instance = new ConsoleManager(App.getInstance());
}
return instance;
}
private static final String CONSOLE_DATE = "HH:mm:ss";
private static final String FILE_DATE = "yyyy/MM/dd HH:mm:ss";
private static final Logger logger = Logger.getLogger("");
private final Map<Colors, String> replacements = new EnumMap<>(Colors.class);
private final Colors[] colors = Colors.values();
private ConsoleReader reader;
private boolean running = true;
private boolean jLine = false;
private ConsoleManager(App server) {
// install Ansi code handler, which makes colors work on Windows
AnsiConsole.systemInstall();
for (Handler h : logger.getHandlers()) {
logger.removeHandler(h);
}
// add log handler which writes to console
logger.addHandler(new FancyConsoleHandler());
// reader must be initialized before standard streams are changed
try {
reader = new ConsoleReader();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Exception initializing console reader", ex);
}
reader.addCompleter(new CommandCompleter());
reader.setHandleUserInterrupt(true);
// set system output streams
System.setOut(new PrintStream(new LoggerOutputStream(Level.INFO), true));
System.setErr(new PrintStream(new LoggerOutputStream(Level.WARNING), true));
// set up colorization replacements
replacements.put(Colors.WHITE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.WHITE).bold().toString());
replacements.put(Colors.BLACK, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLACK).boldOff().toString());
replacements.put(Colors.DARKBLUE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLUE).boldOff().toString());
replacements.put(Colors.DARKGREEN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.GREEN).boldOff().toString());
replacements.put(Colors.RED, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.RED).bold().toString());
replacements.put(Colors.DARKRED, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.RED).boldOff().toString());
replacements.put(Colors.DARKVIOLET, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.MAGENTA).boldOff().toString());
replacements.put(Colors.ORANGE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.YELLOW).bold().toString());
replacements.put(Colors.YELLOW, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.YELLOW).boldOff().toString());
replacements.put(Colors.LIGHTGREEN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.GREEN).bold().toString());
replacements.put(Colors.CYAN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.CYAN).bold().toString());
replacements.put(Colors.LIGHTCYAN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.CYAN).boldOff().toString());
replacements.put(Colors.BLUE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLUE).bold().toString());
replacements.put(Colors.VIOLET, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.MAGENTA).bold().toString());
replacements.put(Colors.DARKGREY, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLACK).bold().toString());
replacements.put(Colors.LIGHTGREY, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.WHITE).boldOff().toString());
replacements.put(Colors.DEFAULT, Ansi.ansi().a(Ansi.Attribute.RESET).toString());
}
public void startConsole(boolean jLine) {
this.jLine = jLine;
Thread thread = new ConsoleCommandThread(this);
thread.setName("ConsoleManager");
//thread.setDaemon(true);
thread.start();
}
public void startFile(String logfile) {
File parent = new File(logfile).getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
logger.warning("Could not create log folder: " + parent);
}
Handler fileHandler = new RotatingFileHandler(logfile);
fileHandler.setFormatter(new DateOutputFormatter(FILE_DATE, false));
logger.addHandler(fileHandler);
}
public void stop() {
running = false;
for (Handler handler : logger.getHandlers()) {
handler.flush();
handler.close();
}
App.shutdown();
}
private String colorize(String string) {
if (!string.contains("\u0003")) {
return string; // no colors in the message
} else if (!jLine || !reader.getTerminal().isAnsiSupported()) {
return Colors.removeAll(string); // color not supported
} else {
String c = "(,(1[0-5]|0?[0-9]))?";
// colorize or strip all colors
for (Colors color : colors) {
if (!string.contains(color.toString())) {
continue;
}
if (replacements.containsKey(color)) {
string = string.replaceAll("(?i)" + color.toString() + c, replacements.get(color));
} else {
string = string.replaceAll("(?i)" + color.toString() + c, "");
}
}
return string + Ansi.ansi().reset().toString();
}
}
private final Pattern regex = Pattern.compile("^(.+)(\\s+?(.*))?", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.MULTILINE | Pattern.COMMENTS);
public void handleConsoleCommand(String string) {
Matcher regexMatcher = regex.matcher(string);
if (regexMatcher.matches()) {
String trigger = regexMatcher.group(1);
String args = regexMatcher.group(3);
for (ExtensionTracker tracker : ExtensionManager.Get().getTrackers().values()) {
if (tracker.getConsoleCommands().keySet().contains(trigger)) {
ConsoleCommand trig = tracker.getConsoleCommands().get(trigger);
System.out.println("Called " + trig.plugin() + ":" + trig.name());
String response = trig.exec(trigger, args);
if (response != null && !response.trim().isEmpty()) {
System.out.println(response);
}
break;
}
}
}
}
private class CommandCompleter implements Completer {
@Override
public int complete(final String buffer, int cursor, List<CharSequence> candidates) {
try {
List<String> completions = null; /*server.getScheduler().syncIfNeeded(new Callable<List<String>>() {
@Override
public List<String> call() throws Exception {
return server.getCommandMap().tabComplete(sender, buffer);
}
});*/
if (completions == null) {
return cursor; // no completions
}
candidates.addAll(completions);
// location to position the cursor at (before autofilling takes place)
return buffer.lastIndexOf(' ') + 1;
} catch (Throwable t) {
logger.log(Level.WARNING, "Error while tab completing", t);
return cursor;
}
}
}
private class ConsoleCommandThread extends Thread {
final ConsoleManager manager;
public ConsoleCommandThread(ConsoleManager manager) {
this.manager = manager;
}
@Override
public void run() {
String command = "";
while (running) {
try {
if (jLine) {
command = reader.readLine(">", null);
} else {
command = reader.readLine();
}
if (command == null || command.trim().length() == 0)
continue;
handleConsoleCommand(command.trim());
} catch (UserInterruptException ex) {
manager.stop();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error while reading commands", ex);
}
}
}
}
private static class LoggerOutputStream extends ByteArrayOutputStream {
private final String separator = System.getProperty("line.separator");
private final Level level;
public LoggerOutputStream(Level level) {
super();
this.level = level;
}
@Override
public synchronized void flush() throws IOException {
super.flush();
String record = this.toString();
super.reset();
if (record.length() > 0 && !record.equals(separator)) {
logger.logp(level, "LoggerOutputStream", "log" + level, record);
}
}
}
private class FancyConsoleHandler extends ConsoleHandler {
public FancyConsoleHandler() {
setFormatter(new DateOutputFormatter(CONSOLE_DATE, true));
setOutputStream(System.out);
}
@Override
public synchronized void flush() {
try {
if (jLine) {
reader.print(ConsoleReader.RESET_LINE + "");
reader.flush();
super.flush();
try {
reader.drawLine();
} catch (Throwable ex) {
reader.getCursorBuffer().clear();
}
reader.flush();
} else {
super.flush();
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "I/O exception flushing console output", ex);
}
}
}
private static class RotatingFileHandler extends StreamHandler {
private final SimpleDateFormat dateFormat;
private final String template;
private final boolean rotate;
private String filename;
public RotatingFileHandler(String template) {
this.template = template;
rotate = template.contains("%D");
dateFormat = new SimpleDateFormat("yyyy-MM-dd");
filename = calculateFilename();
updateOutput();
}
private void updateOutput() {
try {
setOutputStream(new FileOutputStream(filename, true));
} catch (IOException ex) {
logger.log(Level.SEVERE, "Unable to open " + filename + " for writing", ex);
}
}
private void checkRotate() {
if (rotate) {
String newFilename = calculateFilename();
if (!filename.equals(newFilename)) {
filename = newFilename;
// note that the console handler doesn't see this message
super.publish(new LogRecord(Level.INFO, "Log rotating to: " + filename));
updateOutput();
}
}
}
private String calculateFilename() {
return template.replace("%D", dateFormat.format(new Date()));
}
@Override
public synchronized void publish(LogRecord record) {
if (!isLoggable(record)) {
return;
}
checkRotate();
super.publish(record);
super.flush();
}
@Override
public synchronized void flush() {
checkRotate();
super.flush();
}
}
private class DateOutputFormatter extends Formatter {
private final SimpleDateFormat date;
private final boolean color;
public DateOutputFormatter(String pattern, boolean color) {
this.date = new SimpleDateFormat(pattern);
this.color = color;
}
@Override
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public String format(LogRecord record) {
StringBuilder builder = new StringBuilder();
builder.append(date.format(record.getMillis()));
builder.append(" [");
builder.append(record.getLevel().getLocalizedName().toUpperCase());
builder.append("] ");
if (color) {
builder.append(colorize(formatMessage(record)));
} else {
builder.append(formatMessage(record));
}
builder.append('\n');
if (record.getThrown() != null) {
StringWriter writer = new StringWriter();
record.getThrown().printStackTrace(new PrintWriter(writer));
builder.append(writer.toString());
}
return builder.toString();
}
}
}